01-Single-Cell RNA-Seq Analysis: QC & Pre-Processing

Author

Dr Badran Elshenawy

Published

January 1, 2026

Package Installation

  • Seurat is the state-of-the-art package for single-cell RNA-seq analysis in R
  • SeuratData provides curated datasets for learning and testing, including the ifnb dataset we’ll use
  • tidyverse enables efficient data manipulation and visualization
  • scater provides additional QC metrics and utilities
  • clustree helps visualize clustering results across different resolutions
  • BadranSeq provides publication-ready visualizations for single-cell data
# install BiocManager if not already installed
if (!requireNamespace("BiocManager", quietly = TRUE)) {
  install.packages("BiocManager")
}

# install CRAN packages
install.packages(c(
  "tidyverse",
  "Seurat",
  "clustree",
  "remotes",
  "devtools"
))

# install SeuratData for example datasets
remotes::install_github("satijalab/seurat-data")

# install BadranSeq for publication-ready visualizations
devtools::install_github("wolf5996/BadranSeq")

# install Bioconductor packages
BiocManager::install(c("scater", "SingleCellExperiment"))

Directory Structure

  • organizing project files into clear subdirectories makes analyses reproducible and shareable
  • read/ stores input data files (raw counts, annotations, metadata)
  • write/ stores output files, further organized into figures/ and tables/
  • scripts/ contains analysis code (this file lives here)
  • checkpoints/ stores intermediate Seurat objects for long-running analyses
  • using dir.create() with showWarnings = FALSE safely creates directories that may already exist
# create directory structure (safe to run even if directories exist)
dir.create("../read", showWarnings = FALSE)
dir.create("../write", showWarnings = FALSE)
dir.create("../write/figures", showWarnings = FALSE)
dir.create("../write/tables", showWarnings = FALSE)
dir.create("../checkpoints", showWarnings = FALSE)

Introduction

  • in this practical session, we will analyse single-cell RNA-seq data using the Seurat pipeline
  • we will use the ifnb dataset from SeuratData: human PBMCs comparing control vs interferon-beta stimulated cells
  • this dataset is ideal for learning the standard scRNA-seq workflow and differential expression analysis

Loading Packages

library(tidyverse)
library(Seurat)
library(SeuratData)
library(scater)
library(clustree)
library(magrittr)

Handling Package Conflicts

  • dplyr::filter() conflicts with stats::filter() from base R
  • dplyr::select() conflicts with other packages
  • using explicit namespacing (dplyr::filter()) prevents unexpected behavior
library(conflicted)

# set dplyr as the default for common conflicts
conflicts_prefer(dplyr::filter)
conflicts_prefer(dplyr::select)

# resolve other conflicts
conflicts_prefer(base::setdiff)

Overview of the scRNA-Seq Pipeline

  • Quality Control (QC): filter out low-quality cells and uninformative genes
    • cell QC uses library size and detected features
    • gene QC removes features with very low expression across cells
  • Normalization: adjust counts for differences in library size between cells
  • Variable Feature Selection: identify genes driving biological variation
  • Scaling: center and scale data for dimensionality reduction
  • Dimensionality Reduction: PCA followed by UMAP for visualization
  • Clustering: group cells with similar transcriptional profiles

1. Data Import

  • the ifnb dataset contains human PBMCs from control and IFN-beta stimulated conditions
  • InstallData() downloads the dataset; LoadData() loads it into a Seurat object
  • the data comes pre-annotated with cell type labels for exploration
# install and load the ifnb dataset
InstallData("ifnb")
data("ifnb")

# update to current seurat object format (required for SeuratData datasets)
ifnb <- UpdateSeuratObject(ifnb)

# explore the dataset structure
ifnb
An object of class Seurat 
14053 features across 13999 samples within 1 assay 
Active assay: RNA (14053 features, 0 variable features)
 2 layers present: counts, data
  • let’s examine the metadata to understand the experimental design
# view metadata columns
head(ifnb@meta.data)
                  orig.ident nCount_RNA nFeature_RNA stim seurat_annotations
AAACATACATTTCC.1 IMMUNE_CTRL       3017          877 CTRL          CD14 Mono
AAACATACCAGAAA.1 IMMUNE_CTRL       2481          713 CTRL          CD14 Mono
AAACATACCTCGCT.1 IMMUNE_CTRL       3420          850 CTRL          CD14 Mono
AAACATACCTGGTA.1 IMMUNE_CTRL       3156         1109 CTRL                pDC
AAACATACGATGAA.1 IMMUNE_CTRL       1868          634 CTRL       CD4 Memory T
AAACATACGGCATT.1 IMMUNE_CTRL       1581          557 CTRL          CD14 Mono
# check experimental conditions
table(ifnb$stim)

CTRL STIM 
6548 7451 
# check cell type annotations (pre-computed in this dataset)
table(ifnb$seurat_annotations)

   CD14 Mono  CD4 Naive T CD4 Memory T    CD16 Mono            B        CD8 T 
        4362         2504         1762         1044          978          814 
 T activated           NK           DC  B Activated           Mk          pDC 
         633          619          472          388          236          132 
       Eryth 
          55 

2. Quality Control

  • QC is the first and most critical step in any scRNA-seq pipeline
  • poor QC leads to misleading biological conclusions
  • we assess two core metrics per cell: library size and detected features

2.1 Library Size Distribution

  • cells with very low library size may represent empty droplets or debris
  • cells with very high library size may represent doublets (two cells in one droplet)
  • MAD (median absolute deviation) helps set data-driven thresholds
library(ggplot2)
library(magrittr)

# violin plot by condition
VlnPlot(
  ifnb,
  features = "nCount_RNA",
  group.by = "stim",
  pt.size = 0.1
) +
  labs(title = "Library Size by Condition") +
  theme_minimal()

ggsave(
  "../write/figures/library_size_violin.png",
  width = 8,
  height = 6
)

# calculate MAD thresholds
outlier_counts <- isOutlier(
  ifnb@meta.data$nCount_RNA,
  nmads = 3,
  log = TRUE
)
counts_thresholds <- attr(outlier_counts, "thresholds")

# histogram with thresholds
ifnb@meta.data %>%
  ggplot(aes(x = nCount_RNA)) +
  geom_histogram(
    aes(fill = stim),
    bins = 50,
    alpha = 0.7
  ) +
  facet_wrap(~stim, ncol = 1) +
  geom_vline(
    xintercept = counts_thresholds,
    linetype = "dashed",
    color = "red"
  ) +
  labs(
    x = "Library Size",
    y = "Frequency",
    title = "Library Size Distribution"
  ) +
  theme_minimal() +
  scale_x_log10()

ggsave(
  "../write/figures/library_size_histogram.png",
  width = 8,
  height = 6
)

2.2 Detected Features Distribution

  • cells with very few detected genes may be low-quality or empty
  • cells with very many detected genes may be doublets
  • typically correlates with library size
# violin plot
VlnPlot(
  ifnb,
  features = "nFeature_RNA",
  group.by = "stim",
  pt.size = 0.1
) +
  labs(title = "Detected Features by Condition") +
  theme_minimal()

ggsave(
  "../write/figures/detected_features_violin.png",
  width = 8,
  height = 6
)

# calculate MAD thresholds
outlier_features <- isOutlier(
  ifnb@meta.data$nFeature_RNA,
  nmads = 3,
  log = TRUE
)
features_thresholds <- attr(outlier_features, "thresholds")

# histogram with thresholds
ifnb@meta.data %>%
  ggplot(aes(x = nFeature_RNA)) +
  geom_histogram(
    aes(fill = stim),
    bins = 50,
    alpha = 0.7
  ) +
  facet_wrap(~stim, ncol = 1) +
  geom_vline(
    xintercept = features_thresholds,
    linetype = "dashed",
    color = "red"
  ) +
  labs(
    x = "Detected Features",
    y = "Frequency",
    title = "Detected Features Distribution"
  ) +
  theme_minimal()

ggsave(
  "../write/figures/detected_features_histogram.png",
  width = 8,
  height = 6
)

2.3 Combined QC Visualization

  • plotting QC metrics together reveals relationships between them
  • helps identify if thresholds are too stringent or too lenient
  • color by condition to compare distributions
# scatter plot combining QC metrics
ifnb@meta.data %>%
  ggplot(aes(
    x = nCount_RNA,
    y = nFeature_RNA,
    color = stim
  )) +
  geom_point(alpha = 0.5, size = 0.5) +
  facet_wrap(~stim) +
  geom_vline(
    xintercept = counts_thresholds,
    linetype = "dashed",
    color = "red"
  ) +
  geom_hline(
    yintercept = features_thresholds,
    linetype = "dashed",
    color = "red"
  ) +
  labs(
    x = "Library Size",
    y = "Detected Features",
    title = "Combined QC Metrics"
  ) +
  theme_minimal() +
  scale_x_log10()

ggsave(
  "../write/figures/qc_combined_scatter.png",
  width = 10,
  height = 5
)

2.4 Cell Filtering

  • apply QC filters to remove low-quality cells
  • document how many cells are removed at each step
  • be conservative: removing too many cells loses biological signal
# number of cells before filtering
ncol(ifnb)
[1] 13999
# apply filters
ifnb_filtered <- subset(
  ifnb,
  subset = (nCount_RNA > counts_thresholds[1]) &
           (nCount_RNA < counts_thresholds[2]) &
           (nFeature_RNA > features_thresholds[1]) &
           (nFeature_RNA < features_thresholds[2])
)

# number of cells after filtering
ncol(ifnb_filtered)
[1] 13774
# percentage of cells retained
paste0(round(ncol(ifnb_filtered) / ncol(ifnb) * 100, 1), "% of cells retained")
[1] "98.4% of cells retained"

2.5 Gene Filtering

  • remove genes expressed in very few cells
  • these genes add noise without contributing biological signal
  • typical threshold: expressed in at least 3 cells
# number of genes before filtering
nrow(ifnb_filtered)
[1] 14053
# filter genes expressed in at least 3 cells
counts <- GetAssayData(ifnb_filtered, slot = "counts")
gene_filter <- rowSums(counts > 0) >= 3
ifnb_filtered <- ifnb_filtered[gene_filter, ]

# number of genes after filtering
nrow(ifnb_filtered)
[1] 13871

3. Normalization and Variable Feature Selection

  • normalization adjusts for differences in sequencing depth between cells
  • SCTransform is the recommended method: normalizes, finds variable features, and scales in one step
  • variable features are genes with highest cell-to-cell variation and drive clustering
# SCTransform normalization (this may take a few minutes)
ifnb_filtered <- SCTransform(ifnb_filtered, verbose = FALSE)

# check that SCT assay is now active
DefaultAssay(ifnb_filtered)
[1] "SCT"
  • visualize the most variable features
# identify top variable features
top_features <- head(VariableFeatures(ifnb_filtered), 20)

# plot variable features
VariableFeaturePlot(ifnb_filtered) +
  theme_minimal()

ggsave(
  "../write/figures/variable_features.png",
  width = 8,
  height = 6
)

# label top features
LabelPoints(
  plot = VariableFeaturePlot(ifnb_filtered),
  points = top_features,
  repel = TRUE
) +
  theme_minimal()

ggsave(
  "../write/figures/variable_features_labeled.png",
  width = 10,
  height = 8
)

4. Dimensionality Reduction

  • high-dimensional gene expression data is reduced to key components
  • PCA captures linear combinations of genes explaining most variance
  • UMAP provides non-linear visualization for exploring cell populations

4.1 Principal Component Analysis (PCA)

  • PCA is essential for denoising and as input to UMAP and clustering
  • first PCs capture biological variation; later PCs capture noise
  • examining PC loadings reveals genes driving each component
# run PCA
ifnb_filtered <- RunPCA(ifnb_filtered, verbose = FALSE)

# visualize PCA colored by condition using BadranSeq
BadranSeq::do_PcaPlot(
  ifnb_filtered,
  group.by = "stim",
  plot.title = "PCA by Condition"
)

ggsave(
  "../write/figures/pca_condition.png",
  width = 12,
  height = 10
)

# visualize PCA colored by cell type using BadranSeq
BadranSeq::do_PcaPlot(
  ifnb_filtered,
  group.by = "seurat_annotations",
  plot.title = "PCA by Cell Type"
)

ggsave(
  "../write/figures/pca_celltype.png",
  width = 12,
  height = 10
)

4.2 Elbow Plot

  • determine how many PCs to use for downstream analysis
  • look for the “elbow” where additional PCs explain little variance
  • typically 10-30 PCs are used
# elbow plot
BadranSeq::EnhancedElbowPlot(
  object = ifnb_filtered
)

ggsave(
  "../write/figures/elbow_plot.png",
  width = 8,
  height = 6
)

# set number of PCs to use
pcs_use <- 1:20

4.3 UMAP

  • UMAP provides 2D visualization of high-dimensional data
  • uses PCA as input for efficiency
  • preserves local and some global structure
# run UMAP
ifnb_filtered <- RunUMAP(
  ifnb_filtered,
  dims = pcs_use,
  verbose = FALSE
)

# visualize by condition using BadranSeq
BadranSeq::do_UmapPlot(
  ifnb_filtered,
  group.by = "stim",
  plot.title = "UMAP by Condition"
)

ggsave(
  "../write/figures/umap_condition.png",
  width = 12,
  height = 10
)

# visualize by cell type using BadranSeq
BadranSeq::do_UmapPlot(
  ifnb_filtered,
  group.by = "seurat_annotations",
  label = TRUE,
  plot.title = "UMAP by Cell Type"
)

ggsave(
  "../write/figures/umap_celltype.png",
  width = 12,
  height = 10
)

5. Clustering

  • clustering groups cells with similar transcriptional profiles
  • Seurat uses graph-based clustering (Louvain or Leiden algorithm)
  • resolution parameter controls granularity: higher = more clusters

5.1 Finding Neighbors and Clusters

  • FindNeighbors() constructs a KNN graph based on PCA
  • FindClusters() applies community detection to identify clusters
  • test multiple resolutions to find optimal clustering
# find neighbors
ifnb_filtered <- FindNeighbors(
  ifnb_filtered,
  dims = pcs_use,
  verbose = FALSE
)

# find clusters at multiple resolutions
for (res in seq(0.1, 1.0, 0.1)) {
  ifnb_filtered <- FindClusters(
    ifnb_filtered,
    resolution = res,
    verbose = FALSE
  )
}

5.2 Clustering Trees

  • clustering trees show how clusters split at different resolutions
  • helps identify stable clusters vs artifacts of over-clustering
  • SC3 stability index indicates how robust each cluster is
# clustering tree
clustree(ifnb_filtered) +
  theme(legend.position = "bottom")

ggsave(
  "../write/figures/clustree.png",
  width = 14,
  height = 12
)

# clustering tree with stability
clustree(
  ifnb_filtered,
  node_colour = "sc3_stability"
) +
  scale_color_viridis_c(option = "B") +
  theme(legend.position = "bottom")

ggsave(
  "../write/figures/clustree_stability.png",
  width = 14,
  height = 12
)

5.3 Final Clustering

  • select resolution based on clustering tree and biological knowledge
  • visualize clusters on UMAP
# set final clustering resolution
ifnb_filtered <- FindClusters(
  ifnb_filtered,
  resolution = 0.5,
  verbose = FALSE
)

# visualize clusters using BadranSeq
BadranSeq::do_UmapPlot(
  ifnb_filtered,
  label = TRUE,
  plot.title = "UMAP with Clusters (resolution = 0.5)",
  group.by = "seurat_clusters"
)

ggsave(
  "../write/figures/umap_clusters.png",
  width = 12,
  height = 10
)

# compare clusters to known cell types using BadranSeq
BadranSeq::do_UmapPlot(
  ifnb_filtered,
  group.by = "seurat_annotations",
  label = TRUE,
  plot.title = "UMAP with Cell Type Annotations"
)

ggsave(
  "../write/figures/umap_annotations.png",
  width = 12,
  height = 10
)

6. Feature Visualization

  • feature plots show expression of specific genes across cells
  • violin plots compare expression between groups
  • useful for exploring marker genes and biological hypotheses

6.1 Feature Plots

  • visualize expression of known marker genes
  • ISG15 and IFI6 are interferon-stimulated genes expected to be upregulated in STIM
# interferon-stimulated genes
FeaturePlot(
  ifnb_filtered,
  features = c("ISG15", "IFI6", "IFIT1", "IFIT3"),
  ncol = 2
) &
  scale_color_viridis_c(
    option = "B",
    direction = -1
  ) &
  theme_minimal()

ggsave(
  "../write/figures/feature_plot_ifn_genes.png",
  width = 10,
  height = 10
)

# cell type markers
FeaturePlot(
  ifnb_filtered,
  features = c("CD3D", "CD14", "MS4A1", "GNLY"),
  ncol = 2
) &
  scale_color_viridis_c(
    option = "B",
    direction = -1
  ) &
  theme_minimal()

ggsave(
  "../write/figures/feature_plot_markers.png",
  width = 10,
  height = 10
)

6.2 Violin Plots

  • compare expression between conditions or clusters
  • shows distribution of expression values
# compare ISG expression between conditions
VlnPlot(
  ifnb_filtered,
  features = c("ISG15", "IFI6", "IFIT1", "IFIT3"),
  group.by = "stim",
  ncol = 2,
  pt.size = 0
) &
  theme_minimal()

ggsave(
  "../write/figures/violin_ifn_genes.png",
  width = 10,
  height = 8
)

7. Saving Results

  • save the processed Seurat object for future use
  • checkpoints allow resuming analysis without reprocessing
  • using compression reduces file size significantly
# save processed Seurat object with compression
write_rds(
  ifnb_filtered,
  "../checkpoints/ifnb_processed.rds",
  compress = "gz"
)

# to load later:
# ifnb_filtered <- read_rds("../checkpoints/ifnb_processed.rds")

Summary

  • we processed the ifnb dataset through the standard scRNA-seq QC and pre-processing pipeline
  • QC filtering removed low-quality cells while retaining biological signal
  • SCTransform normalization prepared data for downstream analysis
  • PCA and UMAP revealed clear separation between conditions and cell types
  • clustering identified distinct cell populations
  • the processed object is saved for differential expression and pathway analysis in the next module
Step Key Functions
QC isOutlier(), subset()
Normalization SCTransform()
Dim Reduction RunPCA(), RunUMAP()
Clustering FindNeighbors(), FindClusters()
Visualization BadranSeq::do_PcaPlot(), BadranSeq::do_UmapPlot(), VlnPlot()

Session Information

  • always include session info for reproducibility
  • documents R version and all package versions used
sessionInfo()
R version 4.5.0 (2025-04-11)
Platform: x86_64-apple-darwin20
Running under: macOS Sequoia 15.6

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.5-x86_64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.5-x86_64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: Africa/Cairo
tzcode source: internal

attached base packages:
[1] stats4    stats     graphics  grDevices utils     datasets  methods  
[8] base     

other attached packages:
 [1] future_1.68.0               conflicted_1.2.0           
 [3] magrittr_2.0.4              clustree_0.5.1             
 [5] ggraph_2.2.1                scater_1.36.0              
 [7] scuttle_1.18.0              SingleCellExperiment_1.30.1
 [9] SummarizedExperiment_1.38.1 Biobase_2.68.0             
[11] GenomicRanges_1.60.0        GenomeInfoDb_1.44.0        
[13] IRanges_2.42.0              S4Vectors_0.48.0           
[15] BiocGenerics_0.54.0         generics_0.1.4             
[17] MatrixGenerics_1.20.0       matrixStats_1.5.0          
[19] pbmc3k.SeuratData_3.1.4     panc8.SeuratData_3.0.2     
[21] kidneyref.SeuratData_1.0.2  ifnb.SeuratData_3.1.0      
[23] SeuratData_0.2.2.9002       Seurat_5.3.1               
[25] SeuratObject_5.2.0          sp_2.2-0                   
[27] lubridate_1.9.4             forcats_1.0.0              
[29] stringr_1.6.0               dplyr_1.1.4                
[31] purrr_1.2.0                 readr_2.1.5                
[33] tidyr_1.3.1                 tibble_3.3.0               
[35] ggplot2_4.0.1               tidyverse_2.0.0            

loaded via a namespace (and not attached):
  [1] RcppAnnoy_0.0.22          splines_4.5.0            
  [3] later_1.4.4               ggplotify_0.1.2          
  [5] polyclip_1.10-7           fastDummies_1.7.5        
  [7] lifecycle_1.0.4           globals_0.18.0           
  [9] lattice_0.22-6            MASS_7.3-65              
 [11] backports_1.5.0           plotly_4.11.0            
 [13] rmarkdown_2.30            yaml_2.3.11              
 [15] httpuv_1.6.16             otel_0.2.0               
 [17] glmGamPoi_1.20.0          sctransform_0.4.2        
 [19] spam_2.11-1               spatstat.sparse_3.1-0    
 [21] reticulate_1.44.1         cowplot_1.2.0            
 [23] pbapply_1.7-4             RColorBrewer_1.1-3       
 [25] abind_1.4-8               Rtsne_0.17               
 [27] yulab.utils_0.2.0         tweenr_2.0.3             
 [29] rappdirs_0.3.3            GenomeInfoDbData_1.2.14  
 [31] ggrepel_0.9.6             irlba_2.3.5.1            
 [33] listenv_0.10.0            spatstat.utils_3.2-0     
 [35] goftest_1.2-3             RSpectra_0.16-2          
 [37] spatstat.random_3.4-3     fitdistrplus_1.2-4       
 [39] parallelly_1.45.1         DelayedMatrixStats_1.30.0
 [41] codetools_0.2-20          DelayedArray_0.34.1      
 [43] ggforce_0.4.2             tidyselect_1.2.1         
 [45] UCSC.utils_1.4.0          farver_2.1.2             
 [47] ScaledMatrix_1.16.0       viridis_0.6.5            
 [49] spatstat.explore_3.6-0    jsonlite_2.0.0           
 [51] BiocNeighbors_2.2.0       tidygraph_1.3.1          
 [53] progressr_0.18.0          ggridges_0.5.7           
 [55] survival_3.8-3            systemfonts_1.2.3        
 [57] tools_4.5.0               ragg_1.4.0               
 [59] ica_1.0-3                 Rcpp_1.1.0               
 [61] glue_1.8.0                gridExtra_2.3            
 [63] SparseArray_1.8.0         xfun_0.54                
 [65] withr_3.0.2               fastmap_1.2.0            
 [67] digest_0.6.39             rsvd_1.0.5               
 [69] gridGraphics_0.5-1        timechange_0.3.0         
 [71] R6_2.6.1                  mime_0.13                
 [73] colorspace_2.1-1          textshaping_1.0.1        
 [75] scattermore_1.2           tensor_1.5.1             
 [77] spatstat.data_3.1-9       data.table_1.17.8        
 [79] graphlayouts_1.2.2        httr_1.4.7               
 [81] htmlwidgets_1.6.4         S4Arrays_1.8.0           
 [83] uwot_0.2.4                pkgconfig_2.0.3          
 [85] gtable_0.3.6              lmtest_0.9-40            
 [87] S7_0.2.1                  XVector_0.48.0           
 [89] htmltools_0.5.9           dotCall64_1.2            
 [91] scales_1.4.0              png_0.1-8                
 [93] spatstat.univar_3.1-5     knitr_1.50               
 [95] rstudioapi_0.17.1         tzdb_0.5.0               
 [97] reshape2_1.4.5            checkmate_2.3.2          
 [99] nlme_3.1-168              zoo_1.8-14               
[101] cachem_1.1.0              KernSmooth_2.23-26       
[103] parallel_4.5.0            miniUI_0.1.2             
[105] vipor_0.4.7               ggrastr_1.0.2            
[107] pillar_1.11.1             grid_4.5.0               
[109] vctrs_0.6.5               RANN_2.6.2               
[111] promises_1.5.0            BiocSingular_1.24.0      
[113] beachmat_2.24.0           xtable_1.8-4             
[115] cluster_2.1.8.1           beeswarm_0.4.0           
[117] evaluate_1.0.5            BadranSeq_0.0.0.9000     
[119] cli_3.6.5                 compiler_4.5.0           
[121] rlang_1.1.6               crayon_1.5.3             
[123] future.apply_1.20.1       labeling_0.4.3           
[125] fs_1.6.6                  plyr_1.8.9               
[127] ggbeeswarm_0.7.2          SCpubr_3.0.0             
[129] stringi_1.8.7             viridisLite_0.4.2        
[131] deldir_2.0-4              BiocParallel_1.42.0      
[133] assertthat_0.2.1          lazyeval_0.2.2           
[135] spatstat.geom_3.6-1       Matrix_1.7-3             
[137] RcppHNSW_0.6.0            hms_1.1.3                
[139] patchwork_1.3.2           sparseMatrixStats_1.20.0 
[141] shiny_1.12.1              ROCR_1.0-11              
[143] igraph_2.2.1              memoise_2.0.1